package com.googlecode.vmock;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import com.googlecode.vmock.utils.Validate;

/**
 * A call that has been intercepted by {@link CallInterceptorAspect}.
 * 
 * @author marcin.jagus
 */
class InterceptedCall {

    /**
     * Object which called the callable object.
     */
    private Object caller;

    /**
     * Object owning the callable object that was called. Can be null if the callable object is either a constructor, static method or when the call
     * should be mocked for the whole class instead of chosen objects.
     */
    private Object callee;

    /**
     * Callable object. Either method or a constructor
     */
    private AccessibleObject callableObject;

    /**
     * Creates a new <code>InterceptedCall</code> object.
     * 
     * @param caller Object that called the callable object
     * @param callee Owner of the callable object. Can be null if callable object is a static method or a constructor.
     * @param callableObject either {@link Method} or a {@link Constructor.
     */
    public InterceptedCall(Object caller, Object callee, AccessibleObject callableObject) {
        setCaller(caller);
        setCallee(callee);
        setCallableObject(callableObject);
    }

    /**
     * An object which made the call. Can be <code>null</code> if call has been made from a static method or static initializer.
     * 
     * @return An object which made the call or <code>null</code>
     */
    public Object getCaller() {
        return caller;
    }

    /**
     * Modifies the caller of this call.
     * 
     * @param caller New caller. Can be <code>null</code>.
     */
    void setCaller(Object caller) {
        this.caller = caller;
    }

    /**
     * An object being the owner of called method. Can be <code>null</code> if a call is made to the static method.
     * 
     * @return An object being the owner of called method.
     */
    public Object getCallee() {
        return callee;
    }

    /**
     * Modifies the callee of this call.
     * 
     * @param callee New callee. Can be <code>null</code>
     */
    void setCallee(Object callee) {
        this.callee = callee;
    }

    /**
     * Object which was called. Either {@link Method} or a {@link Constructor}.
     * 
     * @return Object which was called. Never <code>null</code>.
     */
    public AccessibleObject getCallableObject() {
        return callableObject;
    }

    /**
     * Modifies callable object.
     * 
     * @param callableObject New callable object. Cannot be <code>null</code>.
     */
    void setCallableObject(AccessibleObject callableObject) {
        Validate.notNull(callableObject, "callableObject == null");
        this.callableObject = callableObject;
    }

    /**
     * Returns <code>true</code> if identities of this call's callee and a given call's callee are equal.
     * 
     * @param ic Given call
     * @return <code>true</code> if identities of this call's callee and a given call's callee are equal
     */
    public boolean isCalleeEqual(InterceptedCall ic) {
        return this.callee == ic.callee;
    }

    /**
     * Returns <code>true</code> if this call's callable object is equal to given call's callable object.
     * 
     * @param ic Given call
     * @return <code>true</code> if this call's callable object is equal to given call's callable object
     */
    public boolean isCallableObjectEqual(InterceptedCall ic) {
        return this.callableObject.equals(ic.callableObject);
    }

    /**
     * Returns <code>true</code> if this call was made to a method that has a variable number of arguments.
     * 
     * @return <code>true</code> if this call was made to a method that has a variable number of arguments
     */
    public boolean isVarArgs() {
        if (callableObject instanceof Method) {
            return ((Method) callableObject).isVarArgs();
        } else if (callableObject instanceof Constructor) {
            return ((Constructor<?>) callableObject).isVarArgs();
        } else {
            return false;
        }
    }

    /**
     * Checks if this call is equal to a given object.
     * <p>
     * Call is equal to another call if and only if both calls have the same calle and callable object.
     * 
     * @param o given object
     */
    @Override
    public boolean equals(Object o) {
        if (o instanceof InterceptedCall) {
            InterceptedCall ic = (InterceptedCall) o;
            return isCalleeEqual(ic) && isCallableObjectEqual(ic);
        } else {
            return false;
        }
    }

    /**
     * Calculates a hash code of this call based on hash code of a callee (if not null) and callable object.
     */
    @Override
    public int hashCode() {
        return 3 * callableObject.hashCode()
                + 7 * System.identityHashCode(callee);
    }

    @Override
    public String toString() {
        return String.format("InterceptedCall{callTo: %s, caller: %s, callee: %s}", callableObject, caller, callee);
    }
}
